﻿Option Strict On

Imports System.Net
Imports System.Net.Sockets
Imports System.Net.Security
Imports System.Security.Cryptography
Imports System.Security.Cryptography.X509Certificates
Imports System.Security.Authentication
Imports System.IO
Imports System.Text
Imports System.Threading
Imports System.Globalization
Imports System.ComponentModel
Imports System.IO.Compression
Imports System.Diagnostics
Imports SFDL.Proxy.Proxy
Imports SFDL.FTP.FTP.Exceptions
Imports System.Text.RegularExpressions


Namespace FTP
    ''' <summary>
    ''' Base abstract class for FtpClient.  Implements FTP network protocols.
    ''' </summary>
    Public MustInherit Class FtpBase
        Implements IDisposable

#Region "Constructors"

        ''' <summary>
        ''' Initializes a new instance of the FtpNetworkAdapter class.
        ''' </summary>
        ''' <param name="port">Port number the adapter is to communicate on.</param>
        ''' <param name="securityProtocol">Value indicating what secure security communications protocol should be used (if any).</param>
        Friend Sub New(ByVal port As Integer, ByVal securityProtocol As FtpSecurityProtocol)
            _port = port
            _securityProtocol = securityProtocol
        End Sub

        ''' <summary>
        ''' Initializes a new instance of the FtpNetworkAdapter class.
        ''' </summary>
        ''' <param name="host">Host the adapter is to communicate on.</param>
        ''' <param name="port">Port number the adapter is to communicate on.</param>
        ''' <param name="securityProtocol">Value indicating what secure security communications protocol should be used (if any).</param>
        Friend Sub New(ByVal host As String, ByVal port As Integer, ByVal securityProtocol As FtpSecurityProtocol)
            _host = host
            _port = port
            _securityProtocol = securityProtocol
        End Sub

#End Region

#Region "Private Variables and Constants"

        Private _commandConn As TcpClient
        Private _commandStream As Stream
        Private _dataConn As TcpClient

        Private _port As Integer
        Private _host As String
        Private _dataTransferMode As TransferMode = TransferMode.Passive

        Private _responseQueue As New FtpResponseQueue()
        Private _response As New FtpResponse()
        Private _responseList As New FtpResponseCollection()

        Private _responseMonitor As Thread
        Shared _reponseMonitorLock As New Object()

        Private _proxy As IProxyClient
        Private _maxUploadSpeed As Integer
        Private _maxDownloadSpeed As Integer

        Private _tcpBufferSize As Integer = TCP_BUFFER_SIZE
        Private _tcpTimeout As Integer = TCP_TIMEOUT

        Private _transferTimeout As Integer = TRANSFER_TIMEOUT
        Private _commandTimeout As Integer = COMMAND_TIMEOUT

        Private _activeListener As TcpListener
        Private _activePort As Integer
        Private _activePortRangeMin As Integer = 50000
        Private _activePortRangeMax As Integer = 50080

        ' secure communications specific 
        Private _securityProtocol As FtpSecurityProtocol = FtpSecurityProtocol.None
        Private _serverCertificate As X509Certificate2
        Private _clientCertificates As New X509CertificateCollection()

        ' data compresion specific
        Private _isCompressionEnabled As Boolean

        ' data integrity specific
        Private _hashAlgorithm As HashingFunction

        ' character encoding
        Private _encoding As Encoding = Encoding.UTF8

        ' thread signal for active mode data transfer
        Private _activeSignal As New ManualResetEvent(False)

        ' async background worker event-based object
        Private _asyncWorker As BackgroundWorker
        Private _asyncCanceled As Boolean

        ''' <summary>
        ''' Default TCP Buffersize 8192 byte
        ''' </summary>
        ''' <remarks></remarks>
        Private Const TCP_BUFFER_SIZE As Integer = 8192
        ''' <summary>
        ''' Default TCP Timeout 30 seconds
        ''' </summary>
        ''' <remarks></remarks>
        Private Const TCP_TIMEOUT As Integer = 30000
        ''' <summary>
        ''' Default Wait for Data Interval 10 milliseconds
        ''' </summary>
        ''' <remarks></remarks>
        Private Const WAIT_FOR_DATA_INTERVAL As Integer = 10
        ''' <summary>
        ''' Default Wait for Command Response Interval 10 milliseconds
        ''' </summary>
        ''' <remarks></remarks>
        Private Const WAIT_FOR_COMMAND_RESPONSE_INTERVAL As Integer = 10
        ''' <summary>
        ''' Default Transfer Timeout 15 seconds
        ''' </summary>
        ''' <remarks></remarks>
        Private Const TRANSFER_TIMEOUT As Integer = 15000
        ''' <summary>
        ''' Default Command Timeout 15 seconds
        ''' </summary>
        ''' <remarks></remarks>
        Private Const COMMAND_TIMEOUT As Integer = 15000
#End Region

#Region "Public Events"

        ''' <summary>
        ''' Server response event.
        ''' </summary>
        Public Event ServerResponse As EventHandler(Of FtpResponseEventArgs)

        ''' <summary>
        ''' Server request event.
        ''' </summary>
        Public Event ClientRequest As EventHandler(Of FtpRequestEventArgs)

        ''' <summary>
        ''' Data transfer progress event.
        ''' </summary>
        Public Event TransferProgress As EventHandler(Of TransferProgressEventArgs)

        ''' <summary>
        ''' Data transfer complete event.
        ''' </summary>
        Public Event TransferComplete As EventHandler(Of TransferCompleteEventArgs)

        ''' <summary>
        ''' Security certificate authentication event.
        ''' </summary>
        Public Event ValidateServerCertificate As EventHandler(Of ValidateServerCertificateEventArgs)

        ''' <summary>
        ''' Connection closed event.
        ''' </summary>
        Public Event ConnectionClosed As EventHandler(Of ConnectionClosedEventArgs)

#End Region

#Region "Public Methods"

        ''' <summary>
        ''' Cancels any asychronous operation that is currently active.
        ''' </summary>
        ''' <seealso cref="FtpClient.FxpCopyAsync"/>
        ''' <seealso cref="FtpClient.GetDirListAsync"/>
        ''' <seealso cref="FtpClient.GetDirListAsync"/>
        ''' <seealso cref="FtpClient.GetDirListDeepAsync"/>
        ''' <seealso cref="FtpClient.GetDirListDeepAsync"/>
        ''' <seealso cref="FtpClient.GetFileAsync"/>
        ''' <seealso cref="FtpClient.GetFileAsync"/>
        ''' <seealso cref="FtpClient.OpenAsync"/>
        ''' <seealso cref="FtpClient.PutFileAsync"/>
        ''' <seealso cref="FtpClient.PutFileAsync"/>
        Public Sub CancelAsync()
            If _asyncWorker IsNot Nothing AndAlso Not _asyncWorker.CancellationPending AndAlso _asyncWorker.IsBusy Then
                _asyncCanceled = True
                _asyncWorker.CancelAsync()
            End If
        End Sub

        ''' <summary>
        ''' Gets the checksum value from the FTP server for the file specified.  Use this value to compare a local checksum to determine file integrity.
        ''' </summary>
        ''' <param name="hash">Hashing function to use.</param>
        ''' <param name="path">Path to the file ont the remote FTP server.</param>
        ''' <return>Hash value in a string format.</return>
        ''' <seealso cref="ComputeChecksum"/>
        Public Function GetChecksum(ByVal hash As HashingFunction, ByVal path As String) As String
            Return GetChecksum(hash, path, 0, 0)
        End Function

        ''' <summary>
        ''' Gets the checksum hash value from the FTP server for the file specified.  Use this value to compare a local checksum to determine file integrity.
        ''' </summary>
        ''' <param name="hash">Hashing function to use.</param>
        ''' <param name="path">Path to the file on the remote FTP server.</param>
        ''' <param name="startPosition">Byte position of where the server should begin computing the hash.</param>
        ''' <param name="endPosition">Byte position of where the server should end computing the hash.</param>
        ''' <return>Checksum hash value in a string format.</return>
        ''' <seealso cref="ComputeChecksum"/>
        Public Function GetChecksum(ByVal hash As HashingFunction, ByVal path As String, ByVal startPosition As Long, ByVal endPosition As Long) As String
            If hash = HashingFunction.None Then
                Throw New ArgumentOutOfRangeException("hash", "must contain a value other than 'Unknown'")
            End If

            If startPosition < 0 Then
                Throw New ArgumentOutOfRangeException("startPosition", "must contain a value greater than or equal to 0")
            End If

            If endPosition < 0 Then
                Throw New ArgumentOutOfRangeException("startPosition", "must contain a value greater than or equal to 0")
            End If

            If startPosition > endPosition Then
                Throw New ArgumentOutOfRangeException("startPosition", "must contain a value less than or equal to endPosition")
            End If

            Dim command As FtpCmd = FtpCmd.Unknown

            Select Case hash
                Case HashingFunction.Crc32
                    command = FtpCmd.Xcrc
                    Exit Select

                Case HashingFunction.Md5
                    command = FtpCmd.Xmd5
                    Exit Select
                Case HashingFunction.Sha1
                    command = FtpCmd.Xsha1
                    Exit Select
            End Select

            ' send request to server to get the hash value for the file
            ' if the restartposition is > 0 then computer the hash on the segment that we resent
            If startPosition > 0 Then
                SendRequest(New FtpRequest(_encoding, command, path, startPosition.ToString(), endPosition.ToString()))
            Else
                SendRequest(New FtpRequest(_encoding, command, path))
            End If

            Return _response.Text
        End Function

        ''' <summary>
        ''' Computes a checksum for a local file.
        ''' </summary>
        ''' <param name="hash">Hashing function to use.</param>
        ''' <param name="localPath">Path to file to perform checksum operation on.</param>
        ''' <return>Hash value in a string format.</return>
        ''' <seealso cref="GetChecksum"/>
        Public Function ComputeChecksum(ByVal hash As HashingFunction, ByVal localPath As String) As String
            If Not File.Exists(localPath) Then
                Throw New ArgumentException("file does not exist.", "localPath")
            End If

            Using fileStream As FileStream = File.OpenRead(localPath)
                Return ComputeChecksum(hash, fileStream)
            End Using
        End Function

        ''' <summary>
        ''' Computes a checksum for a Stream object.
        ''' </summary>
        ''' <param name="hash">Hashing function to use.</param>
        ''' <param name="inputStream">Any System.IO.Stream object.</param>
        ''' <return>Hash value in a string format.</return>
        ''' <remarks>
        ''' The Stream object must allow reads and must allow seeking.
        ''' </remarks>
        ''' <seealso cref="GetChecksum"/>
        Public Function ComputeChecksum(ByVal hash As HashingFunction, ByVal inputStream As Stream) As String
            Return ComputeChecksum(hash, inputStream, 0)
        End Function

        ''' <summary>
        ''' Computes a checksum value for a Stream object.
        ''' </summary>
        ''' <param name="hash">Hashing function to use.</param>
        ''' <param name="inputStream">Any System.IO.Stream object.</param>
        ''' <param name="startPosition">Byte position of where the hash computation should begin.</param>
        ''' <return>Hash value in a string format.</return>
        ''' <remarks>
        ''' The Stream object must allow reads and must allow seeking.
        ''' </remarks>
        ''' <seealso cref="GetChecksum"/>
        Public Shared Function ComputeChecksum(ByVal hash As HashingFunction, ByVal inputStream As Stream, ByVal startPosition As Long) As String
            If hash = HashingFunction.None Then
                Throw New ArgumentOutOfRangeException("hash", "must contain a value other than 'Unknown'")
            End If

            If inputStream Is Nothing Then
                Throw New ArgumentNullException("inputStream")
            End If

            If Not inputStream.CanRead Then
                Throw New ArgumentException("must be readable.  The CanRead property must return a value of 'true'.", "inputStream")
            End If

            If Not inputStream.CanSeek Then
                Throw New ArgumentException("must be seekable.  The CanSeek property must return a value of 'true'.", "inputStream")
            End If

            If startPosition < 0 Then
                Throw New ArgumentOutOfRangeException("startPosition", "must contain a value greater than or equal to 0")
            End If

            Dim hashAlgo As HashAlgorithm = Nothing

            Select Case hash
                Case HashingFunction.Crc32
                    hashAlgo = New FTP.Hashing.Crc32()
                    Exit Select
                Case HashingFunction.Md5
                    hashAlgo = New MD5CryptoServiceProvider()
                    Exit Select
                Case HashingFunction.Sha1
                    hashAlgo = New SHA1CryptoServiceProvider()
                    Exit Select
            End Select

            If startPosition > 0 Then
                inputStream.Position = startPosition
            Else
                inputStream.Position = 0
            End If

            Dim hashArray As Byte() = hashAlgo.ComputeHash(inputStream)
            ' convert byte array to a string
            Dim buffer As New StringBuilder(hashArray.Length)
            For Each hashByte As Byte In hashArray
                buffer.Append(hashByte.ToString("x2"))
            Next

            Return buffer.ToString()

        End Function

#End Region

#Region "Internal Properties"

        Friend ReadOnly Property AsyncWorker() As BackgroundWorker
            Get
                Return _asyncWorker
            End Get
        End Property

#End Region

#Region "Public Properties"

        ''' <summary>
        ''' Gets a value indicating whether an asynchronous operation is canceled.
        ''' </summary>
        ''' <remarks>return true if an asynchronous operation is canceled; otherwise, false.
        ''' </remarks>
        Public ReadOnly Property IsAsyncCanceled() As Boolean
            Get
                Return _asyncCanceled
            End Get
        End Property

        ''' <summary>
        ''' Gets a value indicating whether an asynchronous operation is running.
        ''' </summary>
        ''' <remarks>return true if an asynchronous operation is running; otherwise, false.
        ''' </remarks>
        Public ReadOnly Property IsBusy() As Boolean
            Get
                Return If(_asyncWorker Is Nothing, False, _asyncWorker.IsBusy)
            End Get
        End Property

        ''' <summary>
        ''' Gets or sets the current port number used by the FtpClient to make a connection to the FTP server.
        ''' </summary>
        ''' <remarks>
        ''' The default value is '80'.  This setting can only be changed when the 
        ''' connection to the FTP server is closed.  And FtpException is thrown if this 
        ''' setting is changed when the FTP server connection is open.
        ''' 
        ''' return an integer representing the port number used to connect to a remote server.
        ''' </remarks>
        Public Property Port() As Integer
            Get
                Return _port
            End Get
            Set(ByVal value As Integer)
                If Me.IsConnected Then
                    Throw New FtpException("Port property value can not be changed when connection is open.")
                End If

                _port = value
            End Set
        End Property

        ''' <summary>
        ''' Gets or sets a text value containing the current host used by the FtpClient to make a connection to the FTP server.
        ''' </summary>
        ''' <remarks>
        ''' This value may be in the form of either a host name or IP address.
        ''' This setting can only be changed when the 
        ''' connection to the FTP server is closed.  And FtpException is thrown if this 
        ''' setting is changed when the FTP server connection is open.
        ''' 
        ''' return a string with either the host name or host ip address.
        ''' </remarks>
        Public Property Host() As String
            Get
                Return _host
            End Get
            Set(ByVal value As String)
                If Me.IsConnected Then
                    Throw New FtpException("Host property value can not be changed when connection is open.")
                End If

                _host = value
            End Set
        End Property

        ''' <summary>
        ''' Gets or sets a value indicating what security protocol such as Secure Sock Layer (SSL) should be used.
        ''' </summary>
        ''' <remarks>
        ''' The default value is 'None'.  This setting can only be changed when the 
        ''' connection to the FTP server is closed.  An FtpException is thrown if this 
        ''' setting is changed when the FTP server connection is open.
        ''' 
        ''' return an enumerator specifying the choosen security protocol of either TLS v1.0, SSL v3.0 or SSL v2.0.
        ''' </remarks>
        ''' <seealso cref="SecurityCertificates"/>
        ''' <seealso cref="ValidateServerCertificate" />
        Public Property SecurityProtocol() As FtpSecurityProtocol
            Get
                Return _securityProtocol
            End Get
            Set(ByVal value As FtpSecurityProtocol)
                If Me.IsConnected Then
                    Throw New FtpException("SecurityProtocol property value can not be changed when connection is open.")
                End If

                _securityProtocol = value
            End Set
        End Property

        ''' <summary>
        ''' Get Client certificate collection used when connection with a secured SSL/TSL protocol.  Add your client certificates 
        ''' if required to connect to the remote FTP server.
        ''' </summary>
        ''' <remarks>return a X509CertificateCollection list contains X.509 security certificates.</remarks>
        ''' <seealso cref="SecurityProtocol"/>
        ''' <seealso cref="ValidateServerCertificate" />
        Public ReadOnly Property SecurityCertificates() As X509CertificateCollection
            Get
                Return _clientCertificates
            End Get
        End Property

        ''' <summary>
        ''' Gets or sets a value indicating that the client will use compression when uploading and downloading
        ''' data.
        ''' </summary>
        ''' <remarks>
        ''' This value turns on or off the compression algorithm DEFLATE to facility FTP data compression which is compatible with
        ''' FTP servers that implement compression via the zLib compression software library.  The default value is 'False'.  
        ''' This setting can only be changed when the system is not busy conducting other operations.  
        ''' 
        ''' return True if compression is enabled; otherwise False;
        ''' </remarks>
        Public Property IsCompressionEnabled() As Boolean
            Get
                Return _isCompressionEnabled
            End Get
            Set(ByVal value As Boolean)
                If Me.IsBusy Then
                    Throw New FtpException("IsCompressionEnabled property value can not be changed when the system is busy.")
                End If

                Try
                    If Me.IsConnected Then
                        ' enable compression
                        If value AndAlso value <> _isCompressionEnabled Then
                            CompressionOn()
                        End If

                        ' disable compression
                        If Not value AndAlso value <> _isCompressionEnabled Then
                            CompressionOff()
                        End If
                    End If
                Catch ex As FtpException
                    Throw New FtpDataCompressionException("An error occurred while trying to enable or disable FTP data compression.", ex)
                End Try

                _isCompressionEnabled = value
            End Set
        End Property

        ''' <summary>
        ''' Gets or sets an Integer value representing the maximum upload speed allowed 
        ''' for data transfers in kilobytes per second.
        ''' </summary>
        ''' <remarks>
        ''' Set this value when you would like to throttle back any upload data transfers.
        ''' A value of zero means there is no restriction on how fast data uploads are 
        ''' conducted.  The default value is zero.  This setting is used to throttle data traffic so the FtpClient does
        ''' not consume all available network bandwidth.
        ''' </remarks>
        ''' <seealso cref="MaxDownloadSpeed"/>
        Public Property MaxUploadSpeed() As Integer
            Get
                Return _maxUploadSpeed
            End Get
            Set(ByVal value As Integer)
                If value * 1024 > Int32.MaxValue OrElse value < 0 Then
                    Throw New ArgumentOutOfRangeException("value", "The MaxUploadSpeed property must have a range of 0 to 2,097,152.")
                End If

                _maxUploadSpeed = value
            End Set
        End Property

        ''' <summary>
        ''' Gets or sets an Integer value representing the maximum download speed allowed 
        ''' for data transfers in kilobytes per second.
        ''' </summary>
        ''' <remarks>
        ''' Set this value when you would like to throttle back any download data transfers.
        ''' A value of zero means there is no restriction on how fast data uploads are 
        ''' conducted.  The default value is zero.  This setting is used to throttle data traffic so the FtpClient does
        ''' not consume all available network bandwidth.
        ''' </remarks>
        ''' <seealso cref="MaxUploadSpeed"/>
        Public Property MaxDownloadSpeed() As Integer
            Get
                Return _maxDownloadSpeed
            End Get
            Set(ByVal value As Integer)
                If value * 1024 > Int32.MaxValue OrElse value < 0 Then
                    Throw New ArgumentOutOfRangeException("value", "must have a range of 0 to 2,097,152.")
                End If

                _maxDownloadSpeed = value
            End Set
        End Property

        ''' <summary>
        ''' Gets an Integer value representing the maximum byte count per second
        ''' </summary>
        ''' <value></value>
        ''' <return></return>
        ''' <remarks></remarks>
        Private ReadOnly Property MaxBytesPerSecond(ByVal direction As TransferDirection) As Integer
            Get
                If direction.Equals(TransferDirection.ToClient) Then
                    Return If(Not _maxDownloadSpeed.Equals(0), _maxDownloadSpeed * 1024, 0)
                ElseIf direction.Equals(TransferDirection.ToServer) Then
                    Return If(Not _maxUploadSpeed.Equals(0), _maxUploadSpeed * 1024, 0)
                End If
                Return _tcpBufferSize
            End Get
        End Property


        ''' <summary>
        ''' Gets only the last response from the FTP server.
        ''' </summary>
        ''' <remarks>return a FtpResponse object containing the last FTP server response; other the value null (or Nothing in VB) is returned.</remarks>
        Public ReadOnly Property LastResponse() As FtpResponse
            Get
                Return _response
            End Get
        End Property

        ''' <summary>
        ''' Gets the list of all responses since the last command was issues to the server.
        ''' </summary>
        ''' <remarks>return a FtpResponseCollection list containing all the responses.</remarks>
        Public ReadOnly Property LastResponseList() As FtpResponseCollection
            Get
                Return _responseList
            End Get
        End Property

        ''' <summary>
        ''' Gets or sets the TCP buffer size used when communicating with the FTP server in bytes.
        ''' </summary>
        ''' <remarks>return an integer value representing the buffer size.  The default value is 8192.</remarks>
        Public Property TcpBufferSize() As Integer
            Get
                Return _tcpBufferSize
            End Get
            Set(ByVal value As Integer)
                If value < 1 Then
                    Throw New ArgumentOutOfRangeException("value", "must be greater than 0.")
                End If

                _tcpBufferSize = value
            End Set
        End Property

        ''' <summary>
        ''' Gets or sets the TCP timeout used when communciating with the FTP server in milliseconds.
        ''' </summary>
        ''' <remarks>
        ''' Default value is 30000 (30 seconds).
        ''' </remarks>
        ''' <seealso cref="TransferTimeout"/>
        ''' <seealso cref="CommandTimeout"/>
        Public Property TcpTimeout() As Integer
            Get
                Return _tcpTimeout
            End Get
            Set(ByVal value As Integer)
                If value < 0 Then
                    Throw New ArgumentOutOfRangeException("value", "must be greater than or equal to 0.")
                End If

                _tcpTimeout = value
            End Set
        End Property

        ''' <summary>
        ''' Gets or sets the data transfer timeout used when communicating with the FTP server in milliseconds.
        ''' </summary>
        ''' <remarks>
        ''' Default value is 15000 (15 seconds).
        ''' </remarks>
        ''' <seealso cref="TcpTimeout"/>
        ''' <seealso cref="CommandTimeout"/>
        Public Property TransferTimeout() As Integer
            Get
                Return _transferTimeout
            End Get
            Set(ByVal value As Integer)
                If value < 1 Then
                    Throw New ArgumentOutOfRangeException("value", "must be greater than 0.")
                End If

                _transferTimeout = value
            End Set
        End Property

        ''' <summary>
        ''' Gets or sets the FTP command timeout used when communciating with the FTP server in milliseconds.
        ''' </summary>
        ''' <remarks>
        ''' Default value is 15000 (15 seconds).
        ''' </remarks>
        ''' <seealso cref="TcpTimeout"/>
        ''' <seealso cref="TransferTimeout"/>
        Public Property CommandTimeout() As Integer
            Get
                Return _commandTimeout
            End Get
            Set(ByVal value As Integer)
                If value < 1 Then
                    Throw New ArgumentOutOfRangeException("value", "must be greater than 0.")
                End If

                _commandTimeout = value
            End Set
        End Property

        ''' <summary>
        ''' The beginning port number range used by the FtpClient when opening a local 'Active' port.  The default value is 4051.
        ''' </summary>
        ''' <exception cref="ArgumentOutOfRangeException">Value must be less than or equal to the ActivePortRangeMax value.</exception>
        ''' <remarks>
        ''' When the FtpClient is in 'Active' mode a local port is opened for communications from the FTP server.
        ''' The FtpClient will attempt to open an unused TCP listener port between the ActivePortRangeMin and ActivePortRangeMax values.
        ''' Default value is 50000.
        ''' </remarks>
        ''' <seealso cref="ActivePortRangeMax"/>
        ''' <seealso cref="DataTransferMode"/>
        Public Property ActivePortRangeMin() As Integer
            Get
                Return _activePortRangeMin
            End Get
            Set(ByVal value As Integer)
                If value > _activePortRangeMin Then
                    Throw New ArgumentOutOfRangeException("value", "must be less than the ActivePortRangeMax value.")
                End If

                If value < 1 OrElse value > 65534 Then
                    Throw New ArgumentOutOfRangeException("value", "must be between 1 and 65534.")
                End If

                If Me.IsBusy Then
                    Throw New FtpException("ActivePortRangeMin property value can not be changed when the component is busy.")
                End If

                _activePortRangeMin = value
            End Set
        End Property

        ''' <summary>
        ''' The ending port number range used by the FtpClient when opening a local 'Active' port.  The default value is 4080.
        ''' </summary>
        ''' <exception cref="ArgumentOutOfRangeException">Value must be greater than or equal to the ActivePortRangeMin value.</exception>
        ''' <remarks>
        ''' When the FtpClient is in 'Active' mode a local port is opened for communications from the FTP server.
        ''' The FtpClient will attempt to open an unused TCP listener port between the ActivePortRangeMin and ActivePortRangeMax values.
        ''' Default value is 50080.
        ''' </remarks>
        ''' <seealso cref="ActivePortRangeMin"/>
        ''' <seealso cref="DataTransferMode"/>
        Public Property ActivePortRangeMax() As Integer
            Get
                Return _activePortRangeMax
            End Get
            Set(ByVal value As Integer)
                If value < _activePortRangeMin Then
                    Throw New ArgumentOutOfRangeException("value", "must be greater than the ActivePortRangeMin value.")
                End If

                If value < 1 OrElse value > 65534 Then
                    Throw New ArgumentOutOfRangeException("value", "must be between 1 and 65534.")
                End If

                If Me.IsBusy Then
                    Throw New FtpException("ActivePortRangeMax property value can not be changed when the component is busy.")
                End If

                _activePortRangeMax = value
            End Set
        End Property


        ''' <summary>
        ''' Gets or sets the data transfer mode to either Active or Passive.
        ''' </summary>
        ''' <seealso cref="ActivePortRangeMin"/>
        ''' <seealso cref="ActivePortRangeMax"/>
        Public Property DataTransferMode() As TransferMode
            Get
                Return _dataTransferMode
            End Get
            Set(ByVal value As TransferMode)
                If Me.IsBusy Then
                    Throw New FtpException("DataTransferMode property value can not be changed when the component is busy.")
                End If

                _dataTransferMode = value
            End Set
        End Property

        ''' <summary>
        ''' Gets or sets the the proxy object to use when establishing a connection to the remote FTP server.
        ''' </summary>
        ''' <remarks>Create a proxy object when traversing a firewall.</remarks>
        ''' <code>
        ''' FtpClient ftp = new FtpClient();
        '''
        ''' // create an instance of the client proxy factory for the an ftp client
        ''' ftp.Proxy = (new ProxyClientFactory()).CreateProxyClient(ProxyType.Http, "localhost", 6588);
        '''        
        ''' </code>
        Public Property Proxy() As IProxyClient
            Get
                Return _proxy
            End Get
            Set(ByVal value As IProxyClient)
                _proxy = value
            End Set
        End Property

        ''' <summary>
        ''' Gets the connection status to the FTP server.
        ''' </summary>
        ''' <remarks>return True if the connection is open; otherwise False.</remarks>
        ''' <seealso cref="ConnectionClosed"/>
        Public ReadOnly Property IsConnected() As Boolean
            Get
                If _commandConn Is Nothing OrElse _commandConn.Client Is Nothing Then
                    Return False
                End If

                Dim client As Socket = _commandConn.Client

                If Not client.Connected Then
                    Return False
                End If

                ' this is how you can determine whether a socket is still connected.
                Dim blockingState As Boolean = client.Blocking
                Dim connected As Boolean = True
                Try
                    Dim tmp As Byte() = New Byte(0) {}

                    client.Blocking = False
                    client.Send(tmp, 0, 0)
                Catch e As SocketException
                    ' 10035 == WSAEWOULDBLOCK
                    If Not e.NativeErrorCode.Equals(10035) Then
                        connected = False
                    End If
                Catch generatedExceptionName As ObjectDisposedException
                    connected = False
                Finally
                    Try
                        client.Blocking = blockingState
                    Catch
                        connected = False
                    End Try
                End Try


                Return connected
            End Get
        End Property

        ''' <summary>
        ''' Sets the automatic file integrity setting (checksum) option on all data transfers (upload and download).
        ''' </summary>
        ''' <remarks>
        ''' The FtpClient library will throw an FtpFileIntegrityException if the file integrity value do not match.
        ''' 
        ''' Not all FTP servers support file integrity values such as SHA1, CRC32, or MD5.  If you server does support
        ''' one of these file integrity options, you can set this property and the FtpClient will automatically check
        ''' each file that is transferred to make sure the hash values match.  If the values do not match, an exception
        ''' is thrown.
        ''' </remarks>
        ''' <seealso cref="FtpFileIntegrityException"/>
        ''' <seealso cref="GetChecksum"/>
        ''' <seealso cref="ComputeChecksum"/>
        Public Property AutoChecksumValidation() As HashingFunction
            Get
                Return _hashAlgorithm
            End Get
            Set(ByVal value As HashingFunction)
                _hashAlgorithm = value
            End Set
        End Property

        ''' <summary>
        ''' Gets or sets the character encoding used when sending commands to the FTP server or receiving directory listing information.
        ''' This encoding value does not affect the encoding of files being transferred.  The default value is UTF-8.  Some older FTP servers
        ''' require different character encoding such as UTF-7.
        ''' </summary>
        ''' <remarks>
        ''' To set this value use the .NET System.Text.Encoding class. The following example sets the character encoding to UTF-7.  In addition,
        ''' other encodings can be specified by string name using the static method System.Text.Encoding.GetEncoding();
        ''' <code>
        '''     FtpClient.CharacterEncoding = System.Text.Encoding.UTF7;
        ''' </code>
        ''' </remarks>
        Public Property CharacterEncoding() As Encoding
            Get
                Return _encoding
            End Get
            Set(ByVal value As Encoding)
                If value Is Nothing Then
                    Throw New ArgumentNullException("value")
                End If
                _encoding = value
            End Set
        End Property

#End Region

#Region "Internal Protected Methods"

        ''' <summary>
        ''' Send a FTP command request to the server.
        ''' </summary>
        ''' <param name="request"></param>
        Friend Sub SendRequest(ByVal request As FtpRequest)
            If _commandConn Is Nothing OrElse _commandConn.Connected = False Then
                Throw New FtpConnectionClosedException("Connection is closed.")
            End If

            ' clear out any responses that might have been pending from a previous
            ' failed operation
            DontWaitForHappyCodes()

            RaiseEvent ClientRequest(Me, New FtpRequestEventArgs(request))

            Dim buffer As Byte() = request.GetBytes()

            Try
                _commandStream.Write(buffer, 0, buffer.Length)
            Catch ex As IOException
                Throw New FtpConnectionBrokenException("Connection is broken.  Failed to send command.", ex)
            End Try

            ' most commands will have happy codes but the quote() command does not 
            If request.HasHappyCodes Then
                WaitForHappyCodes(request.GetHappyCodes())
            Else
                ' when there are no happy codes given the we have to give the server some time to response
                ' since we really don't know what response is the correct one
                If request.Command <> FtpCmd.Quit Then
                    Thread.Sleep(2000)
                End If
                DontWaitForHappyCodes()
            End If
        End Sub

        Private Sub DontWaitForHappyCodes()
            If _responseQueue.Count = 0 Then
                Return
            End If

            _responseList.Clear()
            While _responseQueue.Count > 0
                Dim response As FtpResponse = _responseQueue.Dequeue()
                _responseList.Add(response)
                RaiseServerResponseEvent(New FtpResponse(response))
            End While
            _response = _responseList.GetLast()
        End Sub




        ''' <summary>
        ''' creates a new async worker object for the async events to use.
        ''' </summary>
        Friend Sub CreateAsyncWorker()
            If _asyncWorker IsNot Nothing Then
                _asyncWorker.Dispose()
            End If
            _asyncWorker = Nothing
            _asyncCanceled = False
            _asyncWorker = New BackgroundWorker()
        End Sub

        ''' <summary>
        ''' Closes all connections to the FTP server.
        ''' </summary>
        Friend Sub CloseAllConnections()
            CloseDataConn()
            CloseCommandConn()
            AbortMonitorThread()
        End Sub

        ''' <summary>
        ''' The monitor thread should close automatically once the command connection is terminated.  If it does not close properly, force it to close.
        ''' </summary>
        Private Sub AbortMonitorThread()
            _responseMonitor.Abort()
        End Sub

        '  open a connection to the server
        Friend Sub OpenCommandConn()
            '  create a new tcp client object 
            CreateCommandConnection()
            StartCommandMonitorThread()

            System.Threading.Thread.Sleep(1000)

            If _securityProtocol = FtpSecurityProtocol.Ssl2Explicit OrElse _securityProtocol = FtpSecurityProtocol.Ssl3Explicit OrElse _securityProtocol = FtpSecurityProtocol.Tls1Explicit OrElse _securityProtocol = FtpSecurityProtocol.Tls1OrSsl3Explicit Then
                CreateSslExplicitCommandStream()
            End If

            If _securityProtocol = FtpSecurityProtocol.Ssl2Implicit OrElse _securityProtocol = FtpSecurityProtocol.Ssl3Implicit OrElse _securityProtocol = FtpSecurityProtocol.Tls1Implicit OrElse _securityProtocol = FtpSecurityProtocol.Tls1OrSsl3Implicit Then
                CreateSslImplicitCommandStream()
            End If

            ' test to see if this is an asychronous operation and if so make sure 
            ' the user has not requested the operation to be canceled
            If IsAsyncCancellationPending() Then
                Return
            End If

            ' this check screws up secure connections so we have to ignore it when secure connections are enabled
            If _securityProtocol = FtpSecurityProtocol.None Then
                WaitForHappyCodes(FtpResponseCode.ServiceReadyForNewUser)
            End If

        End Sub

        Friend Sub TransferData(ByVal direction As TransferDirection, ByVal request As FtpRequest, ByVal data As Stream)
            TransferData(direction, request, data, 0)
        End Sub

        Friend Sub TransferData(ByVal direction As TransferDirection, ByVal request As FtpRequest, ByVal data As Stream, ByVal restartPosition As Long)
            If _commandConn Is Nothing OrElse _commandConn.Connected = False Then
                Throw New FtpConnectionClosedException("Connection is closed.")
            End If

            If request Is Nothing Then
                Throw New ArgumentNullException("request", "value is required")
            End If

            If data Is Nothing Then
                Throw New ArgumentNullException("data", "value is required")
            End If

            Select Case direction
                Case TransferDirection.ToClient
                    If Not data.CanWrite Then
                        Throw New FtpDataTransferException("Data transfer error.  Data conn does not allow write operation.")
                    End If
                    Exit Select
                Case TransferDirection.ToServer
                    If Not data.CanRead Then
                        Throw New FtpDataTransferException("Data transfer error.  Data conn does not allow read operation.")
                    End If
                    Exit Select
            End Select

            ' if this is a restart then the data stream must support seeking
            If restartPosition > 0 AndAlso Not data.CanSeek Then
                Throw New FtpDataTransferException("Data transfer restart error.  Data conn does not allow seek operation.")
            End If

            Try
                ' create a thread to begin the process of opening a data connection to the remote server
                OpenDataConn()

                '  check for a restart position 
                If restartPosition > 0 Then
                    ' instruct the server to restart file transfer at the same position where the output stream left off
                    SendRequest(New FtpRequest(_encoding, FtpCmd.Rest, restartPosition.ToString(CultureInfo.InvariantCulture)))

                    ' set the data stream to the same position as the server
                    data.Position = restartPosition
                End If

                ' send the data transfer command that requires a separate data connection to be established to transmit data
                SendRequest(request)

                ' wait for the data connection thread to signal back that a connection has been established
                WaitForDataConn()

                ' test to see if the data connection failed to be established sometime the active connection fails due to security settings on the ftp server
                If _dataConn Is Nothing Then
                    Throw New FtpDataConnectionException("Unable to establish a data connection to the destination.  The destination may have refused the connection.")
                End If

                ' create the data stream object - handles creation of SslStream and DeflateStream objects as well
                Dim conn As Stream = _dataConn.GetStream()

                ' test to see if we need to enable ssl/tls explicit mode
                If _securityProtocol <> FtpSecurityProtocol.None Then
                    conn = CreateSslStream(conn)
                End If

                ' test to see if we need to enable compression by using the DeflateStream
                If _isCompressionEnabled Then
                    conn = CreateZlibStream(direction, conn)
                End If

                ' based on the direction of the data transfer we need to handle the input and output streams
                Select Case direction
                    Case TransferDirection.ToClient
                        TransferBytes(conn, data, direction)
                        Exit Select
                    Case TransferDirection.ToServer
                        TransferBytes(data, conn, direction)
                        Exit Select
                End Select
            Finally
                ' attempt to close the data connection
                CloseDataConn()
            End Try

            ' if no errors occurred and this is not a quoted command then we will wait for the server to send a closing connection message
            WaitForHappyCodes(FtpResponseCode.ClosingDataConnection)

            ' integrity check
            If _hashAlgorithm <> HashingFunction.None AndAlso request.IsFileTransfer Then
                DoIntegrityCheck(request, data, restartPosition)
            End If
        End Sub

        Private Function CreateZlibStream(ByVal direction As TransferDirection, ByVal stream As Stream) As Stream
            Dim deflateStream As DeflateStream = Nothing

            Select Case direction
                Case TransferDirection.ToClient
                    deflateStream = New DeflateStream(stream, CompressionMode.Decompress, True)
                    ' zlib fix to ignore first two bytes of header data 
                    deflateStream.BaseStream.ReadByte()
                    deflateStream.BaseStream.ReadByte()
                    Exit Select

                Case TransferDirection.ToServer
                    deflateStream = New DeflateStream(stream, CompressionMode.Compress, True)
                    ' this is a fix for the DeflateStream class only when sending compressed data to the server.  
                    ' Zlib has two bytes of data attached to the header that we have to write before processing the data stream.
                    deflateStream.BaseStream.WriteByte(120)
                    deflateStream.BaseStream.WriteByte(218)
                    Exit Select
            End Select
            Return deflateStream
        End Function

        Friend Function TransferText(ByVal request As FtpRequest) As String
            Using output As Stream = New MemoryStream()
                TransferData(TransferDirection.ToClient, request, output)
                output.Position = 0
                Dim reader As New StreamReader(output, _encoding)
                Return reader.ReadToEnd()
            End Using
        End Function

        Friend Sub CompressionOn()
            Try
                SendRequest(New FtpRequest(_encoding, FtpCmd.Mode, "Z"))
            Catch ex As Exception
                Throw New FtpDataCompressionException("Unable to enable compression (MODE Z) on the destination.", ex)
            End Try
        End Sub

        Friend Sub CompressionOff()
            Try
                SendRequest(New FtpRequest(_encoding, FtpCmd.Mode, "S"))
            Catch ex As Exception
                Throw New FtpDataCompressionException("Unable to disable compression (MODE S) on the destination.", ex)
            End Try
        End Sub

#End Region

#Region "Private Methods"

        Private Sub StartCommandMonitorThread()
            ' start the monitor thread which pumps FtpResponse objects on the FtpResponseQueue
            _responseMonitor = New Thread(New ThreadStart(AddressOf MonitorCommandConnection))
            _responseMonitor.Name = "FtpBase Response Monitor"
            _responseMonitor.Start()
        End Sub

        Private Function IsAsyncCancellationPending() As Boolean
            If _asyncWorker IsNot Nothing AndAlso _asyncWorker.CancellationPending Then
                _asyncCanceled = True
                Return True
            End If
            Return False
        End Function

        Private Sub TransferBytes(ByVal input As Stream, ByVal output As Stream, ByVal direction As TransferDirection)
            Dim buffersize As Integer = _tcpBufferSize * 10
            Dim buffer As Byte() = New Byte(buffersize - 1) {}
            Dim bytesTotal As Long = 0
            Dim bytesRead As Integer = 0
            Dim start As DateTime = DateTime.Now
            Dim elapsed As New TimeSpan(0)
            Dim bytesPerSec As Integer = 0
            Dim maxBytesPerSecond As Integer = Me.MaxBytesPerSecond(direction)

            Dim changePause As Integer = 0

            While True
                bytesRead = input.Read(buffer, 0, buffersize)

                If bytesRead = 0 Then
                    Exit While
                End If

                bytesTotal += bytesRead
                output.Write(buffer, 0, bytesRead)

                ' calculate some statistics
                elapsed = DateTime.Now.Subtract(start)
                bytesPerSec = CInt(Math.Truncate(If(elapsed.TotalSeconds < 1, bytesTotal, bytesTotal / elapsed.TotalSeconds)))

                '  if the consumer subscribes to transfer progress event then fire it
                RaiseEvent TransferProgress(Me, New TransferProgressEventArgs(bytesRead, bytesPerSec, elapsed))

                ' test to see if this is an asychronous operation and if so make sure 
                ' the user has not requested the operation to be canceled
                If IsAsyncCancellationPending() Then
                    Throw New FtpAsynchronousOperationException("Asynchronous operation canceled by user.")
                End If

                ' throttle the transfer if necessary
                If ThrottleByteTransfer(maxBytesPerSecond, bytesTotal, elapsed, bytesPerSec) AndAlso changePause.Equals(10) Then
                    maxBytesPerSecond = Me.MaxBytesPerSecond(direction)
                End If
            End While

            '  if the consumer subscribes to transfer complete event then fire it
            RaiseEvent TransferComplete(Me, New TransferCompleteEventArgs(bytesTotal, bytesPerSec, elapsed))
        End Sub

        Private Function ThrottleByteTransfer(ByVal maxBytesPerSecond As Integer, ByVal bytesTotal As Long, ByVal elapsed As TimeSpan, ByVal bytesPerSec As Integer) As Boolean
            ' we only throttle if the maxBytesPerSecond is not zero (zero turns off the throttle)
            If maxBytesPerSecond > 0 Then
                ' we only throttle if our through-put is higher than what we want
                If bytesPerSec > maxBytesPerSecond Then
                    Dim elapsedMilliSec As Double = If(elapsed.TotalSeconds = 0, elapsed.TotalMilliseconds, elapsed.TotalSeconds * 1000)

                    ' need to calc a delay in milliseconds for the throttle wait based on how fast the 
                    ' transfer is relative to the speed it needs to be
                    Dim millisecDelay As Double = (bytesTotal \ (maxBytesPerSecond \ 1000) - elapsedMilliSec)

                    ' can only sleep to a max of an Int32 so we need to check this since bytesTotal is a long value
                    ' this should never be an issue but never say never
                    If millisecDelay > Int32.MaxValue Then
                        millisecDelay = Int32.MaxValue
                    End If

                    ' go to sleep
                    Thread.Sleep(CInt(Math.Truncate(millisecDelay)))
                    Return True
                End If
                Return False
            End If
            Return True
        End Function

        Private Sub CreateCommandConnection()
            If _host Is Nothing OrElse _host.Length = 0 Then
                Throw New FtpException("An FTP Host must be specified before opening connection to FTP destination.  Set the appropriate value using the Host property on the FtpClient object.")
            End If

            Try
                '  test to see if we should use the user supplied proxy object
                '  to create the connection
                If _proxy IsNot Nothing Then
                    _commandConn = _proxy.CreateConnection(_host, _port)
                Else
                    ' _commandConn = New TcpClient(_host, _port)
                    _commandConn = New TcpClient

                    _commandConn.SendTimeout = _tcpTimeout
                    _commandConn.ReceiveTimeout = _tcpTimeout
                    _commandConn.Connect(_host, _port)

                End If
            Catch pex As ProxyException
                If _commandConn IsNot Nothing Then
                    _commandConn.Close()
                End If

                Throw New FtpProxyException(String.Format(CultureInfo.InvariantCulture, "A proxy error occurred while creating connection to FTP destination {0} on port {1}.", _host, _port.ToString(CultureInfo.InvariantCulture)), pex)
            Catch ex As Exception
                If _commandConn IsNot Nothing Then
                    _commandConn.Close()
                End If

                Throw New FtpConnectionOpenException(String.Format(CultureInfo.InvariantCulture, "An error occurred while opening a connection to FTP destination {0} on port {1}.", _host, _port.ToString(CultureInfo.InvariantCulture)), ex)
            End Try

            ' set command connection buffer sizes and timeouts
            _commandConn.ReceiveBufferSize = _tcpBufferSize
            _commandConn.ReceiveTimeout = _tcpTimeout
            _commandConn.SendBufferSize = _tcpBufferSize
            _commandConn.SendTimeout = _tcpTimeout

            ' set the command stream object
            _commandStream = _commandConn.GetStream()
        End Sub




        Private Sub CloseCommandConn()
            If _commandConn Is Nothing Then
                Return
            End If
            Try
                If _commandConn.Connected Then
                    '  send the quit command to the server
                    SendRequest(New FtpRequest(_encoding, FtpCmd.Quit))
                End If
                _commandConn.Close()
            Catch
            End Try

            _commandConn = Nothing
        End Sub


        Private Sub WaitForHappyCodes(ByVal ParamArray happyResponseCodes As FtpResponseCode())
            WaitForHappyCodes(_commandTimeout, happyResponseCodes)
        End Sub

        ''' <summary>
        ''' Waits until a happy code has been returned by the FTP server or times out.
        ''' </summary>
        ''' <param name="timeout">Maximum time to wait before timing out.</param>
        ''' <param name="happyResponseCodes">Server response codes to wait for.</param>
        Protected Friend Sub WaitForHappyCodes(ByVal timeout As Integer, ByVal ParamArray happyResponseCodes As FtpResponseCode())
            _responseList.Clear()
            Do
                Dim response As FtpResponse = GetNextCommandResponse(timeout)
                _responseList.Add(response)
                RaiseServerResponseEvent(New FtpResponse(response))

                If Not response.IsInformational Then
                    If IsHappyResponse(response, happyResponseCodes) Then
                        Exit Do
                    End If

                    If IsUnhappyResponse(response) Then
                        _response = response
                        Throw New FtpResponseException("FTP command failed.", response)
                    End If
                End If
            Loop While True

            _response = _responseList.GetLast()
        End Sub

        Private Sub RaiseServerResponseEvent(ByVal response As FtpResponse)
            RaiseEvent ServerResponse(Me, New FtpResponseEventArgs(response))
        End Sub

        Private Sub RaiseConnectionClosedEvent()
            RaiseEvent ConnectionClosed(Me, New ConnectionClosedEventArgs())
        End Sub

        Private Function IsUnhappyResponse(ByVal response As FtpResponse) As Boolean
            If response.Code = FtpResponseCode.ServiceNotAvailableClosingControlConnection OrElse _
                response.Code = FtpResponseCode.CannotOpenDataConnection OrElse _
                response.Code = FtpResponseCode.ConnectionClosedSoTransferAborted OrElse _
                response.Code = FtpResponseCode.RequestedFileActionNotTaken OrElse _
                response.Code = FtpResponseCode.RequestedActionAbortedDueToLocalErrorInProcessing OrElse _
                response.Code = FtpResponseCode.RequestedActionNotTakenInsufficientStorage OrElse _
                response.Code = FtpResponseCode.SyntaxErrorCommandUnrecognized OrElse _
                response.Code = FtpResponseCode.SyntaxErrorInParametersOrArguments OrElse _
                response.Code = FtpResponseCode.CommandNotImplemented OrElse _
                response.Code = FtpResponseCode.BadSequenceOfCommands OrElse _
                response.Code = FtpResponseCode.CommandNotImplementedForThatParameter OrElse _
                response.Code = FtpResponseCode.NotLoggedIn OrElse _
                response.Code = FtpResponseCode.NeedAccountForStoringFiles OrElse _
                response.Code = FtpResponseCode.RequestedActionNotTakenFileUnavailable OrElse _
                response.Code = FtpResponseCode.RequestedActionAbortedPageTypeUnknown OrElse _
                response.Code = FtpResponseCode.RequestedFileActionAbortedExceededStorageAllocation OrElse _
                response.Code = FtpResponseCode.RequestedActionNotTakenFileNameNotAllowed Then
                Return True
            Else
                Return False
            End If
        End Function

        Private Function IsHappyResponse(ByVal response As FtpResponse, ByVal happyResponseCodes As FtpResponseCode()) As Boolean
            ' always return true if there are no responses to validate
            If happyResponseCodes.Length = 0 Then
                Return True
            End If

            For j As Integer = 0 To happyResponseCodes.Length - 1
                If happyResponseCodes(j) = response.Code Then
                    Return True
                End If
            Next
            Return False
        End Function

        'Private Sub MonitorCommandConnection()
        '    Dim buffer As Byte() = New Byte(_tcpBufferSize - 1) {}
        '    Dim response As New StringBuilder()
        '    While IsConnected
        '        SyncLock _reponseMonitorLock
        '            Thread.Sleep(WAIT_FOR_COMMAND_RESPONSE_INTERVAL)
        '            Try
        '                If _commandConn IsNot Nothing AndAlso _commandConn.GetStream().DataAvailable Then
        '                    Dim bytes As Integer = _commandStream.Read(buffer, 0, _tcpBufferSize)
        '                    Dim [partial] As String = _encoding.GetString(buffer, 0, bytes)
        '                    response.Append([partial])
        '                    If [partial].EndsWith(vbCr & vbLf) Then
        '                        '  parse out the response code sent back from the server
        '                        '  in some cases more than one response can be sent with
        '                        '  each line separated with a crlf pair.
        '                        Dim responseArray As String() = SplitResponse(response.ToString())
        '                        For i As Integer = 0 To responseArray.Length - 1
        '                            _responseQueue.Enqueue(New FtpResponse(responseArray(i)))
        '                        Next

        '                        response.Remove(0, response.Length)
        '                    End If
        '                End If
        '            Catch
        '            End Try
        '        End SyncLock
        '    End While

        '    RaiseConnectionClosedEvent()

        'End Sub

        Private Sub MonitorCommandConnection()
            Dim buffer As Byte() = New Byte(_tcpBufferSize - 1) {}
            Dim response As New StringBuilder()
            Dim code421Detected As Boolean = False

            While IsConnected
                SyncLock _reponseMonitorLock
                    Thread.Sleep(WAIT_FOR_COMMAND_RESPONSE_INTERVAL)
                    Try
                        If _commandConn IsNot Nothing AndAlso _commandConn.GetStream().DataAvailable Then
                            Dim bytes As Integer = _commandStream.Read(buffer, 0, _tcpBufferSize)
                            Dim [partial] As String = _encoding.GetString(buffer, 0, bytes)
                            response.Append([partial])

                            ' write out the debug response
                            Debug.Write("R: " & [partial])

                            If Not [partial].EndsWith(vbLf) Then

                                Continue While

                            End If

                            '  parse out the response code sent back from the server
                            '  in some cases more than one response can be sent with
                            '  each line separated with a crlf pair.
                            Dim responseArray As String() = SplitResponse(response.ToString())
                            For i As Integer = 0 To responseArray.Length - 1
                                Dim r As New FtpResponse(responseArray(i))
                                _responseQueue.Enqueue(r)
                                ' if a 421 response code is detected then the server has closed the connection
                                If r.Code = FtpResponseCode.ServiceNotAvailableClosingControlConnection Then
                                    code421Detected = True
                                End If
                            Next

                            response.Remove(0, response.Length)

                            ' if code 421 was detected then close the command connection
                            If code421Detected Then
                                CloseCommandConn()
                            End If
                        End If
                    Catch
                    End Try
                End SyncLock

            End While

            RaiseConnectionClosedEvent()

        End Sub


        Private Function GetNextCommandResponse(ByVal timeout As Integer) As FtpResponse
            Dim sleepTime As Integer = 0
            While _responseQueue.Count = 0
                If Not IsConnected Then
                    Throw New FtpConnectionClosedException("Connection is closed.")
                End If

                If IsAsyncCancellationPending() Then
                    Throw New FtpAsynchronousOperationException("Asynchronous operation canceled.")
                End If

                Thread.Sleep(WAIT_FOR_DATA_INTERVAL)
                sleepTime += WAIT_FOR_DATA_INTERVAL
                If sleepTime > timeout Then
                    Throw New FtpCommandResponseTimeoutException("A timeout occurred while waiting for the destination to send a response.  The last reponse from the destination is '" & Convert.ToString(_response.Text) & "'")
                End If
            End While

            ' return next response object from the queue
            Return _responseQueue.Dequeue()
        End Function

        Private Function SplitResponse(ByVal response As String) As String()
            Return response.Split(New Char() {ControlChars.Cr, ControlChars.Lf}, StringSplitOptions.RemoveEmptyEntries)
        End Function

        Private Function GetNextActiveModeListenerPort() As Integer
            If _activePort < _activePortRangeMin OrElse _activePort > _activePortRangeMax Then
                _activePort = _activePortRangeMin
            Else
                _activePort += 1
            End If

            Return _activePort
        End Function

        Private Sub CreateActiveConn()
            Dim localHost As String = Dns.GetHostName()
            Dim localAddresses As IPAddress() = Dns.GetHostAddresses(localHost)
            Dim localAddr As IPAddress = Nothing
            For Each addr As IPAddress In localAddresses
                If addr.AddressFamily = AddressFamily.InterNetwork Then
                    localAddr = addr
                    'ACHTUNG: Hier müssen wir noch ne andere Lösung finden.
                    Exit For
                End If
            Next

            If localAddr Is Nothing Then
                Throw New Exception("Local host does not have an IPv4 address")
            End If

            ' Set the event to nonsignaled state.
            _activeSignal.Reset()

            Dim success As Boolean = False
            Dim listenerPort As Integer = 0

            Do
                Dim failureCnt As Integer = 0

                Try
                    listenerPort = GetNextActiveModeListenerPort()
                    _activeListener = New TcpListener(localAddr, listenerPort)
                    _activeListener.Start()
                    success = True
                Catch socketError As SocketException
                    If socketError.ErrorCode = 10048 AndAlso System.Threading.Interlocked.Increment(failureCnt) < _activePortRangeMax - _activePortRangeMin Then
                        _activeListener.[Stop]()
                    Else
                        Throw New FtpDataConnectionException(String.Format(CultureInfo.InvariantCulture, "An error occurred while trying to create an active connection on host {0} port {1}", localHost, listenerPort.ToString(CultureInfo.InvariantCulture)), socketError)
                    End If
                End Try
            Loop While Not success

            Dim addrBytes As Byte() = localAddr.GetAddressBytes()
            Dim dataPortInfo As String = String.Format(CultureInfo.InvariantCulture, "{0},{1},{2},{3},{4},{5}", addrBytes(0).ToString(CultureInfo.InvariantCulture), addrBytes(1).ToString(CultureInfo.InvariantCulture), addrBytes(2).ToString(CultureInfo.InvariantCulture), addrBytes(3).ToString(CultureInfo.InvariantCulture), _
             listenerPort \ 256, listenerPort Mod 256)

            ' Accept the connection.  BeginAcceptSocket() creates the accepted socket.
            _activeListener.BeginAcceptTcpClient(New AsyncCallback(AddressOf Me.AcceptTcpClientCallback), _activeListener)

            '  send a command to the server instructing it to connect to
            '  the local ip address and port that the tcplistener is bound to
            Try
                SendRequest(New FtpRequest(_encoding, FtpCmd.Port, dataPortInfo))
            Catch fex As FtpException
                Throw New FtpDataConnectionException(String.Format("An error occurred while issuing data port command '{0}' on an active FTP connection.", dataPortInfo), fex)
            End Try
        End Sub

        ' async callback that occurs once the server has connected to the client listener data connection
        Private Sub AcceptTcpClientCallback(ByVal ar As IAsyncResult)
            ' Get the listener that handles the client request.
            Dim listener As TcpListener = DirectCast(ar.AsyncState, TcpListener)

            ' make sure that the server didn't close the connection on us or just refuse to allow an active connection
            ' for security considerations and other purposes some servers will refuse active connections
            Try
                _dataConn = listener.EndAcceptTcpClient(ar)
            Catch
            End Try

            ' signal the calling thread to continue now that the data connection is open
            _activeSignal.[Set]()
        End Sub

        Private Sub OpenDataConn()
            '  create the approiate ftp data connection depending on how the ftp client should send 
            '  or receive data from the ftp server
            If _dataTransferMode = TransferMode.Active Then
                CreateActiveConn()
            Else
                CreatePassiveConn()
            End If
        End Sub

        Private Sub CloseDataConn()
            '  close the tcpclient data connection object
            If _dataConn IsNot Nothing Then
                Try
                    _dataConn.Close()
                Catch
                Finally
                    _dataConn = Nothing
                End Try
            End If

            '  stop the tcplistner object if we used it for an active data transfer where we
            '  are listing and the server makes a connection to the client and pushed data 
            If _dataTransferMode = TransferMode.Active AndAlso _activeListener IsNot Nothing Then
                Try
                    _activeListener.[Stop]()
                Catch
                Finally
                    _activeListener = Nothing
                End Try
            End If
        End Sub

        Private Sub WaitForDataConn()
            '  if the transfer mode is active then we have to open a listener and wait for the server to connect before sending
            '  or receiving data but if the transfer mode is passive then we make the connection (non blocking) 
            '  and therefore there is no need to wait for the server and our signal that the server has connected 
            '  - we already have the connection object since the tcpclient object blocks until the server accepts the connection
            If _dataTransferMode = TransferMode.Active Then
                ' wait until a data connection is made before continuing based on a thread blocking signal object
                If Not _activeSignal.WaitOne(_transferTimeout, False) Then
                    If _response.Code = FtpResponseCode.CannotOpenDataConnection Then
                        Throw New FtpDataConnectionException(String.Format(CultureInfo.InvariantCulture, "The ftp destination was unable to open a data connection to the ftp client on port {0}.", _activePort))
                    Else
                        Throw New FtpDataConnectionTimeoutException("The data connection timed out waiting for data to transfer from the destination.")
                    End If
                End If
            Else
                Return
            End If
        End Sub

        Private Sub CreatePassiveConn()
            '  send command to get passive port to be used from the server
            Try
                SendRequest(New FtpRequest(_encoding, FtpCmd.Pasv))
            Catch fex As FtpException
                Throw New FtpDataConnectionException("An error occurred while issuing up a passive FTP connection command.", fex)
            End Try

            '  get the port on the end
            '  to calculate the port number we must multiple the 5th value by 256 and then add the 6th value
            '  example:  
            '       Client> PASV
            '       Server> 227 Entering Passive Mode (123,45,67,89,158,26)
            '  In the example of PASV mode the server has said it will be listening on IP address 123.45.67.89 
            '  on TCP port 40474 for the data channel. (Note: the destinationPort is the 158,26 pair and is: 158x256 + 26 = 40474).

            '  get the begin and end positions to extract data from the response string
            Dim startIdx As Integer = _response.Text.IndexOf("(") + 1
            Dim endIdx As Integer = _response.Text.IndexOf(")")

            '  parse the transfer connection data from the ftp server response
            Dim data As String() = _response.Text.Substring(startIdx, endIdx - startIdx).Split(","c)

            ' build the data host name from the server response
            Dim passiveHost As String = data(0) & "." & data(1) & "." & data(2) & "." & data(3)
            ' extract and convert the port number from the server response
            Dim passivePort As Integer = Int32.Parse(data(4), CultureInfo.InvariantCulture) * 256 + Int32.Parse(data(5), CultureInfo.InvariantCulture)


            'Fix not routable IP-Addresses
            Dim regex As Regex = New Regex("(^10\.)|(^172\.1[6-9]\.)|(^172\.2[0-9]\.)|(^172\.3[0-1]\.)|(^192\.168\.)|(^127\.0\.0\.1)")
            Dim match As Match = regex.Match(passiveHost)
            If match.Success Then
                Console.WriteLine("Private IP? Are you kidding me? -> Using Server IP!")
                passiveHost = _host
            End If

            Try
                '  create a new tcp client object and use proxy if supplied
                If _proxy IsNot Nothing Then
                    _dataConn = _proxy.CreateConnection(passiveHost, passivePort)
                Else
                    _dataConn = New TcpClient(passiveHost, passivePort)
                End If

                _dataConn.ReceiveBufferSize = _tcpBufferSize
                _dataConn.ReceiveTimeout = _tcpTimeout
                _dataConn.SendBufferSize = _tcpBufferSize
                _dataConn.SendTimeout = _tcpTimeout
            Catch ex As Exception
                Throw New FtpDataConnectionException(String.Format(CultureInfo.InvariantCulture, "An error occurred while opening passive data connection to destination '{0}' on port '{1}'.", passiveHost, passivePort), ex)
            End Try
        End Sub

        ''' <summary>
        ''' Creates an SSL or TLS secured stream.
        ''' </summary>
        ''' <param name="stream">Unsecured stream.</param>
        ''' <return>Secured stream</return>
        Private Function CreateSslStream(ByVal stream As Stream) As Stream

            ' create an SSL or TLS stream that will close the client's stream
            Dim ssl As New SslStream(stream, True, New RemoteCertificateValidationCallback(AddressOf secureStream_ValidateServerCertificate), Nothing)

            ' choose the protocol
            Dim protocol As SslProtocols = SslProtocols.None
            Select Case _securityProtocol
                Case FtpSecurityProtocol.Tls1OrSsl3Explicit, FtpSecurityProtocol.Tls1OrSsl3Implicit
                    protocol = SslProtocols.[Default]
                    Exit Select
                Case FtpSecurityProtocol.Ssl2Explicit, FtpSecurityProtocol.Ssl2Implicit
                    protocol = SslProtocols.Ssl2
                    Exit Select
                Case FtpSecurityProtocol.Ssl3Explicit, FtpSecurityProtocol.Ssl3Implicit
                    protocol = SslProtocols.Ssl3
                    Exit Select
                Case FtpSecurityProtocol.Tls1Explicit, FtpSecurityProtocol.Tls1Implicit
                    protocol = SslProtocols.Tls
                    Exit Select
                Case Else
                    Throw New FtpSecureConnectionException(String.Format("Unhandled FtpSecurityProtocol type '{0}'.", _securityProtocol.ToString()))
            End Select

            ' The server name must match the name on the server certificate.

                Try

                    ' authenicate the client
                    ssl.AuthenticateAsClient(_host, _clientCertificates, protocol, True)

                Catch authEx As AuthenticationException
                    Throw New FtpAuthenticationException("Secure FTP session certificate authentication failed.", authEx)

                Catch ex As Exception
                End Try


            Return ssl
        End Function

        ' the following method is invoked by the RemoteCertificateValidationDelegate.
        Private Function secureStream_ValidateServerCertificate(ByVal sender As Object, ByVal certificate As X509Certificate, ByVal chain As X509Chain, ByVal sslPolicyErrors__1 As SslPolicyErrors) As Boolean
            ' if it is the same certificate we have already approved then don't validate it again
            If _serverCertificate IsNot Nothing AndAlso certificate.GetCertHashString() = _serverCertificate.GetCertHashString() Then
                Return True
            End If

            ' invoke the ValidateServerCertificate event if the user is subscribing to it
            ' ignore our own logic and let the user decide if the certificate is valid or not

            Try
                Dim args As New ValidateServerCertificateEventArgs(New X509Certificate2(certificate.GetRawCertData()), chain, sslPolicyErrors__1)
                RaiseEvent ValidateServerCertificate(Me, args)
                ' make a copy of the certificate due to sharing violations
                If args.IsCertificateValid Then
                    _serverCertificate = New X509Certificate2(certificate.GetRawCertData())
                End If
                Return args.IsCertificateValid
            Catch
                ' analyze the policy errors and decide if the certificate should be accepted or not.
                If (sslPolicyErrors__1 And SslPolicyErrors.RemoteCertificateNameMismatch) = SslPolicyErrors.RemoteCertificateNameMismatch Then
                    Throw New FtpCertificateValidationException(String.Format("Certificate validation failed.  The host name '{0}' does not match the name on the security certificate '{1}'.  To override this behavior, subscribe to the ValidateServerCertificate event to validate certificates.", _host, certificate.Issuer))
                End If

                If sslPolicyErrors__1 = SslPolicyErrors.None OrElse (sslPolicyErrors__1 And SslPolicyErrors.RemoteCertificateChainErrors) = SslPolicyErrors.RemoteCertificateChainErrors Then
                    ' make a copy of the server certificate due to sharing violations
                    _serverCertificate = New X509Certificate2(certificate.GetRawCertData())
                    Return True
                Else
                    Return False
                End If
            End Try

        End Function

        Private Sub CreateSslExplicitCommandStream()
            Try
                ' send authentication type request
                Dim authCommand As String = ""
                Select Case _securityProtocol
                    Case FtpSecurityProtocol.Tls1OrSsl3Explicit, FtpSecurityProtocol.Ssl3Explicit, FtpSecurityProtocol.Ssl2Explicit
                        authCommand = "SSL"
                        Exit Select
                    Case FtpSecurityProtocol.Tls1Explicit
                        authCommand = "TLS"
                        Exit Select
                End Select

                Debug.Assert(authCommand.Length > 0, "auth command should have a value - make sure every enum option in auth command has a corresponding value")

                SendRequest(New FtpRequest(_encoding, FtpCmd.Auth, authCommand))

                Thread.Sleep(1000)

                ' set the active command stream to the ssl command stream object
                SyncLock _reponseMonitorLock

                    Dim _stream As IO.Stream

                    _stream = _commandConn.GetStream

                    Thread.Sleep(500)

                    _commandStream = CreateSslStream(_stream)

                End SyncLock

                SendRequest(New FtpRequest(_encoding, FtpCmd.Pbsz, "0"))
                SendRequest(New FtpRequest(_encoding, FtpCmd.Prot, "P"))
            Catch fauth As FtpAuthenticationException
                Throw New FtpSecureConnectionException(String.Format("An ftp authentication exception occurred while setting up a explicit ssl/tls command stream.  {0}", fauth.Message), _response, fauth)
            Catch fex As FtpException
                Throw New FtpSecureConnectionException(String.Format("An error occurred while setting up a explicit ssl/tls command stream.  {0}", fex.Message), _response, fex)
            End Try

        End Sub

        Private Sub CreateSslImplicitCommandStream()
            Try
                ' set the active command stream to the ssl command stream object
                SyncLock _reponseMonitorLock
                    _commandStream = CreateSslStream(_commandConn.GetStream())
                End SyncLock
            Catch fauth As FtpAuthenticationException
                Throw New FtpSecureConnectionException(String.Format("An ftp authentication exception occurred while setting up a implicit ssl/tls command stream.  {0}", fauth.Message), _response, fauth)
            Catch fex As FtpException
                Throw New FtpSecureConnectionException(String.Format("An error occurred while setting up a implicit ssl/tls command stream.  {0}", fex.Message), _response, fex)
            End Try

        End Sub

        Private Sub DoIntegrityCheck(ByVal request As FtpRequest, ByVal stream As Stream, ByVal restartPosition As Long)
            ' get the file path from the request argument
            Dim path As String = request.Arguments(0)
            Dim startPos As Long = restartPosition
            Dim endPos As Long = stream.Length

            Dim streamHash As String = ComputeChecksum(_hashAlgorithm, stream, startPos)
            Dim serverHash As String = GetChecksum(_hashAlgorithm, path, startPos, endPos)

            ' string compare the dataHash to the server hash value and see if they are the same
            If String.Compare(streamHash, serverHash, StringComparison.InvariantCultureIgnoreCase) <> 0 Then
                Throw New FtpFileIntegrityException(String.Format("File integrity check failed.  The destination integrity value '{0}' for the file '{1}' did not match the data transfer integrity value '{2}'.", serverHash, path, streamHash))
            End If
        End Sub

#End Region

#Region "Destructors"

        ''' <summary>
        ''' Disposes all objects and connections.
        ''' </summary>
        Public Sub Dispose() Implements IDisposable.Dispose
            Dispose(True)
            GC.SuppressFinalize(Me)
        End Sub


        ''' <summary>
        ''' Dispose Method.
        ''' </summary>
        ''' <param name="disposing"></param>
        Protected Overridable Sub Dispose(ByVal disposing As Boolean)
            If disposing Then
                If _asyncWorker IsNot Nothing AndAlso _asyncWorker.IsBusy Then
                    _asyncWorker.CancelAsync()
                End If

                If _asyncWorker IsNot Nothing Then
                    _asyncWorker.Dispose()
                End If

                If _activeListener IsNot Nothing Then
                    _activeListener.[Stop]()
                End If

                If _dataConn IsNot Nothing AndAlso _dataConn.Connected Then
                    _dataConn.Close()
                End If

                If _commandConn IsNot Nothing AndAlso _commandConn.Connected Then
                    _commandConn.Close()
                End If

                If _activeSignal IsNot Nothing Then
                    _activeSignal.Close()
                End If

                If _responseMonitor IsNot Nothing AndAlso _responseMonitor.IsAlive Then
                    _responseMonitor.Abort()
                End If
            End If
        End Sub

#End Region

    End Class
End Namespace